package net.stuffrepos.userdesktopdatabase.helper;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import net.stuffrepos.util.cache.CacheableMapValue;
import net.stuffrepos.util.cache.CacheableValue;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.tool.hbm2ddl.SchemaExport;

/**
 *
 * @author Eduardo H. Bogoni <eduardobogoni@gmail.com>
 */
public class HibernateHelper {

    private static final Log log = LogFactory.getLog(HibernateHelper.class);
    private AnnotationConfiguration configuration;
    private SessionFactory sessionFactory;
    private final String connectionUrl;
    private final CacheableMapValue<Thread, SessionManager> sessions = new CacheableMapValue<Thread, SessionManager>() {

        @Override
        protected SessionManager calculate(Thread key) {
            return new SessionManager();
        }
    };
    private HashSet<Class> entities;

    public HibernateHelper(String connectionUrl, Collection<Class> entities) {
        this.entities = new HashSet<Class>(entities);
        this.connectionUrl = connectionUrl;
    }

    private AnnotationConfiguration getConfiguration() {
        if (configuration == null) {
            AnnotationConfiguration ac = new AnnotationConfiguration();

            log.debug("Build Hibernate configuration...");

            for (Class e : entities) {
                ac.addAnnotatedClass(e);
            }

            ac.setProperty("hibernate.query.substitutions", "yes 'Y', no 'N'");
            ac.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
            ac.setProperty("hibernate.connection.driver_class", "org.hsqldb.jdbcDriver");
            ac.setProperty("hibernate.connection.username", "sa");
            ac.setProperty("hibernate.connection.password", "");
            ac.setProperty("hibernate.connection.pool_size", "1");
            ac.setProperty("hibernate.proxool.pool_alias", "pool1");
            ac.setProperty("hibernate.current_session_context_class", "thread");
            ac.setProperty("hibernate.show_sql", "false");
            ac.setProperty("hibernate.format_sql", "false");
            ac.setProperty("hibernate.max_fetch_depth", "1");
            ac.setProperty("hibernate.jdbc.batch_versioned_data", "true");
            ac.setProperty("hibernate.jdbc.use_streams_for_binary", "true");
            ac.setProperty("hibernate.cache.region_prefix", "hibernate.test");
            ac.setProperty("hibernate.cache.provider_class", "org.hibernate.cache.HashtableCacheProvider");
            ac.setProperty("hibernate.connection.url", connectionUrl);

            log.debug("Hibernate configuration builded");

            configuration = ac;
        }

        return configuration;
    }

    private synchronized SessionFactory getSessionFactory() {
        if (sessionFactory == null) {
            sessionFactory = getConfiguration().buildSessionFactory();
        }
        return sessionFactory;
    }

    public <ObjectType extends Serializable> ObjectType save(final ObjectType object) throws Exception {
        executeTransaction(new Transaction<ObjectType>() {

            @Override
            public ObjectType execute(Session session) throws Exception {
                return (ObjectType) session.save(object);
            }
        });
        return object;

    }

    public void exportSchema() {
        SchemaExport export = new SchemaExport(getConfiguration());
        export.create(true, false);
    }

    public <ReturnType> ReturnType executeTransaction(Transaction<ReturnType> transaction) {
        return sessions.getValue(Thread.currentThread()).executeTransaction(transaction);
    }

    public void close() {
        try {
            sessions.clear();
        } catch (org.hibernate.SessionException ex) {
            log.warn("Exception on closing session", ex);
        }
    }

    private String stackTraceToString(StackTraceElement[] elements) {
        StringBuilder b = new StringBuilder();

        for (StackTraceElement e : elements) {
            b.append(e.toString()).append('\n');
        }

        return b.toString();
    }

    public static interface Transaction<ReturnType> {

        public ReturnType execute(Session session) throws Exception;
    }

    private class SessionManager {

        private StackTraceElement[] busy;
        private Thread busyThread;
        private final CacheableValue<Session> session = new CacheableValue<Session>() {

            @Override
            protected Session calculate() {
                return getSessionFactory().openSession();
            }
        };

        public <ReturnType> ReturnType executeTransaction(Transaction<ReturnType> transaction) {
            try {
                if (busy != null) {
                    throw new RuntimeException(
                            "Transaction executer is already busy\n"
                            + "Busy Thread: " + busyThread.getName() + "\n"
                            + "Busy Stack: " + stackTraceToString(busy) + "\n"
                            + "Active Thread: " + Thread.currentThread().getName() + "\n"
                            + "Active Stack: " + stackTraceToString(Thread.currentThread().getStackTrace()) + "\n");
                }

                busyThread = Thread.currentThread();
                busy = Thread.currentThread().getStackTrace();

                org.hibernate.Transaction hibernateTransaction = session.getValue().beginTransaction();

                ReturnType result = transaction.execute(session.getValue());

                session.getValue().flush();
                hibernateTransaction.commit();
                return result;
            } catch (Exception ex) {
                if (ex instanceof RuntimeException) {
                    throw (RuntimeException) ex;
                } else {
                    throw new RuntimeException(ex);
                }
            } finally {
                busy = null;
                busyThread = null;
            }
        }
    }
}
